1   /*
2    * Copyright (c) 2004, 2007, Oracle and/or its affiliates. All rights reserved.
3    * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4    *
5    * This code is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU General Public License version 2 only, as
7    * published by the Free Software Foundation.  Oracle designates this
8    * particular file as subject to the "Classpath" exception as provided
9    * by Oracle in the LICENSE file that accompanied this code.
10   *
11   * This code is distributed in the hope that it will be useful, but WITHOUT
12   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14   * version 2 for more details (a copy is included in the LICENSE file that
15   * accompanied this code).
16   *
17   * You should have received a copy of the GNU General Public License version
18   * 2 along with this work; if not, write to the Free Software Foundation,
19   * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20   *
21   * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22   * or visit www.oracle.com if you need additional information or have any
23   * questions.
24   */
25  
26  package sun.tools.jconsole;
27  
28  import java.awt.*;
29  import java.awt.event.*;
30  import java.io.*;
31  import java.lang.management.*;
32  import java.lang.reflect.*;
33  
34  import javax.swing.*;
35  import javax.swing.border.*;
36  import javax.swing.event.*;
37  
38  import java.util.*;
39  import java.util.concurrent.*;
40  import java.util.List;
41  
42  import sun.awt.*;
43  
44  import static sun.tools.jconsole.OverviewPanel.*;
45  import static sun.tools.jconsole.Resources.*;
46  import static sun.tools.jconsole.Utilities.*;
47  
48  
49  @SuppressWarnings("serial")
50  class ThreadTab extends Tab implements ActionListener, DocumentListener, ListSelectionListener {
51      PlotterPanel threadMeter;
52      TimeComboBox timeComboBox;
53      JTabbedPane threadListTabbedPane;
54      DefaultListModel listModel;
55      JTextField filterTF;
56      JLabel messageLabel;
57      JSplitPane threadsSplitPane;
58      HashMap<Long, String> nameCache = new HashMap<Long, String>();
59  
60      private ThreadOverviewPanel overviewPanel;
61      private boolean plotterListening = false;
62  
63  
64      private static final String threadCountKey   = "threadCount";
65      private static final String peakKey          = "peak";
66  
67      private static final String threadCountName   = Resources.getText("Live Threads");
68      private static final String peakName          = Resources.getText("Peak");
69  
70      private static final Color  threadCountColor = Plotter.defaultColor;
71      private static final Color  peakColor        = Color.red;
72  
73      private static final Border thinEmptyBorder  = new EmptyBorder(2, 2, 2, 2);
74  
75      private static final String infoLabelFormat = "ThreadTab.infoLabelFormat";
76  
77  
78      /*
79        Hierarchy of panels and layouts for this tab:
80  
81          ThreadTab (BorderLayout)
82  
83              North:  topPanel (BorderLayout)
84  
85                          Center: controlPanel (FlowLayout)
86                                      timeComboBox
87  
88              Center: plotterPanel (BorderLayout)
89  
90                          Center: plotter
91  
92      */
93  
94  
95      public static String getTabName() {
96          return Resources.getText("Threads");
97      }
98  
99      public ThreadTab(VMPanel vmPanel) {
100         super(vmPanel, getTabName());
101 
102         setLayout(new BorderLayout(0, 0));
103         setBorder(new EmptyBorder(4, 4, 3, 4));
104 
105         JPanel topPanel     = new JPanel(new BorderLayout());
106         JPanel plotterPanel = new JPanel(new VariableGridLayout(0, 1, 4, 4, true, true));
107 
108         add(topPanel, BorderLayout.NORTH);
109         add(plotterPanel,  BorderLayout.CENTER);
110 
111         JPanel controlPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 20, 5));
112         topPanel.add(controlPanel, BorderLayout.CENTER);
113 
114         threadMeter = new PlotterPanel(Resources.getText("Number of Threads"),
115                                        Plotter.Unit.NONE, true);
116         threadMeter.plotter.createSequence(threadCountKey, threadCountName,  threadCountColor, true);
117         threadMeter.plotter.createSequence(peakKey,        peakName,         peakColor,        true);
118         setAccessibleName(threadMeter.plotter,
119                           getText("ThreadTab.threadPlotter.accessibleName"));
120 
121         plotterPanel.add(threadMeter);
122 
123         timeComboBox = new TimeComboBox(threadMeter.plotter);
124         controlPanel.add(new LabeledComponent(Resources.getText("Time Range:"),
125                                               getMnemonicInt("Time Range:"),
126                                               timeComboBox));
127 
128         listModel = new DefaultListModel();
129 
130         JTextArea textArea = new JTextArea();
131         textArea.setBorder(thinEmptyBorder);
132         textArea.setEditable(false);
133         setAccessibleName(textArea,
134                           getText("ThreadTab.threadInfo.accessibleName"));
135         JList list = new ThreadJList(listModel, textArea);
136 
137         Dimension di = new Dimension(super.getPreferredSize());
138         di.width = Math.min(di.width, 200);
139 
140         JScrollPane threadlistSP = new JScrollPane(list);
141         threadlistSP.setPreferredSize(di);
142         threadlistSP.setBorder(null);
143 
144         JScrollPane textAreaSP = new JScrollPane(textArea);
145         textAreaSP.setBorder(null);
146 
147         threadListTabbedPane = new JTabbedPane(JTabbedPane.TOP);
148         threadsSplitPane  = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
149                                            threadlistSP, textAreaSP);
150         threadsSplitPane.setOneTouchExpandable(true);
151         threadsSplitPane.setBorder(null);
152 
153         JPanel firstTabPanel = new JPanel(new BorderLayout());
154         firstTabPanel.setOpaque(false);
155 
156         JPanel firstTabToolPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 2));
157         firstTabToolPanel.setOpaque(false);
158 
159         filterTF = new PromptingTextField("Filter", 20);
160         filterTF.getDocument().addDocumentListener(this);
161         firstTabToolPanel.add(filterTF);
162 
163         JSeparator separator = new JSeparator(JSeparator.VERTICAL);
164         separator.setPreferredSize(new Dimension(separator.getPreferredSize().width,
165                                                  filterTF.getPreferredSize().height));
166         firstTabToolPanel.add(separator);
167 
168         JButton detectDeadlockButton = new JButton(Resources.getText("Detect Deadlock"));
169         detectDeadlockButton.setMnemonic(getMnemonicInt("Detect Deadlock"));
170         detectDeadlockButton.setActionCommand("detectDeadlock");
171         detectDeadlockButton.addActionListener(this);
172         detectDeadlockButton.setToolTipText(getText("Detect Deadlock.toolTip"));
173         firstTabToolPanel.add(detectDeadlockButton);
174 
175         messageLabel = new JLabel();
176         firstTabToolPanel.add(messageLabel);
177 
178         firstTabPanel.add(threadsSplitPane, BorderLayout.CENTER);
179         firstTabPanel.add(firstTabToolPanel, BorderLayout.SOUTH);
180         threadListTabbedPane.addTab(Resources.getText("Threads"), firstTabPanel);
181 
182         plotterPanel.add(threadListTabbedPane);
183     }
184 
185     private long oldThreads[] = new long[0];
186 
187     public SwingWorker<?, ?> newSwingWorker() {
188         final ProxyClient proxyClient = vmPanel.getProxyClient();
189 
190         if (!plotterListening) {
191             proxyClient.addWeakPropertyChangeListener(threadMeter.plotter);
192             plotterListening = true;
193         }
194 
195         return new SwingWorker<Boolean, Object>() {
196             private int tlCount;
197             private int tpCount;
198             private long ttCount;
199             private long[] threads;
200             private long timeStamp;
201 
202             public Boolean doInBackground() {
203                 try {
204                     ThreadMXBean threadMBean = proxyClient.getThreadMXBean();
205 
206                     tlCount = threadMBean.getThreadCount();
207                     tpCount = threadMBean.getPeakThreadCount();
208                     if (overviewPanel != null) {
209                         ttCount = threadMBean.getTotalStartedThreadCount();
210                     } else {
211                         ttCount = 0L;
212                     }
213 
214                     threads = threadMBean.getAllThreadIds();
215                     for (long newThread : threads) {
216                         if (nameCache.get(newThread) == null) {
217                             ThreadInfo ti = threadMBean.getThreadInfo(newThread);
218                             if (ti != null) {
219                                 String name = ti.getThreadName();
220                                 if (name != null) {
221                                     nameCache.put(newThread, name);
222                                 }
223                             }
224                         }
225                     }
226                     timeStamp = System.currentTimeMillis();
227                     return true;
228                 } catch (IOException e) {
229                     return false;
230                 } catch (UndeclaredThrowableException e) {
231                     return false;
232                 }
233             }
234 
235             protected void done() {
236                 try {
237                     if (!get()) {
238                         return;
239                     }
240                 } catch (InterruptedException ex) {
241                     return;
242                 } catch (ExecutionException ex) {
243                     if (JConsole.isDebug()) {
244                         ex.printStackTrace();
245                     }
246                     return;
247                 }
248 
249                 threadMeter.plotter.addValues(timeStamp, tlCount, tpCount);
250                 threadMeter.setValueLabel(tlCount+"");
251 
252                 if (overviewPanel != null) {
253                     overviewPanel.updateThreadsInfo(tlCount, tpCount, ttCount, timeStamp);
254                 }
255 
256                 String filter = filterTF.getText().toLowerCase(Locale.ENGLISH);
257                 boolean doFilter = (filter.length() > 0);
258 
259                 ArrayList<Long> l = new ArrayList<Long>();
260                 for (long t : threads) {
261                     l.add(t);
262                 }
263                 Iterator<Long> iterator = l.iterator();
264                 while (iterator.hasNext()) {
265                     long newThread = iterator.next();
266                     String name = nameCache.get(newThread);
267                     if (doFilter && name != null &&
268                         name.toLowerCase(Locale.ENGLISH).indexOf(filter) < 0) {
269 
270                         iterator.remove();
271                     }
272                 }
273                 long[] newThreads = threads;
274                 if (l.size() < threads.length) {
275                     newThreads = new long[l.size()];
276                     for (int i = 0; i < newThreads.length; i++) {
277                         newThreads[i] = l.get(i);
278                     }
279                 }
280 
281 
282                 for (long oldThread : oldThreads) {
283                     boolean found = false;
284                     for (long newThread : newThreads) {
285                         if (newThread == oldThread) {
286                             found = true;
287                             break;
288                         }
289                     }
290                     if (!found) {
291                         listModel.removeElement(oldThread);
292                         if (!doFilter) {
293                             nameCache.remove(oldThread);
294                         }
295                     }
296                 }
297 
298                 // Threads are in reverse chronological order
299                 for (int i = newThreads.length - 1; i >= 0; i--) {
300                     long newThread = newThreads[i];
301                     boolean found = false;
302                     for (long oldThread : oldThreads) {
303                         if (newThread == oldThread) {
304                             found = true;
305                             break;
306                         }
307                     }
308                     if (!found) {
309                         listModel.addElement(newThread);
310                     }
311                 }
312                 oldThreads = newThreads;
313             }
314         };
315     }
316 
317     long lastSelected = -1;
318 
319     public void valueChanged(ListSelectionEvent ev) {
320         ThreadJList list = (ThreadJList)ev.getSource();
321         final JTextArea textArea = list.textArea;
322 
323         Long selected = (Long)list.getSelectedValue();
324         if (selected == null) {
325             if (lastSelected != -1) {
326                 selected = lastSelected;
327             }
328         } else {
329             lastSelected = selected;
330         }
331         textArea.setText("");
332         if (selected != null) {
333             final long threadID = selected;
334             workerAdd(new Runnable() {
335                 public void run() {
336                     ProxyClient proxyClient = vmPanel.getProxyClient();
337                     StringBuilder sb = new StringBuilder();
338                     try {
339                         ThreadMXBean threadMBean = proxyClient.getThreadMXBean();
340                         ThreadInfo ti = null;
341                         MonitorInfo[] monitors = null;
342                         if (proxyClient.isLockUsageSupported() &&
343                               threadMBean.isObjectMonitorUsageSupported()) {
344                             // VMs that support the monitor usage monitoring
345                             ThreadInfo[] infos = threadMBean.dumpAllThreads(true, false);
346                             for (ThreadInfo info : infos) {
347                                 if (info.getThreadId() == threadID) {
348                                     ti = info;
349                                     monitors = info.getLockedMonitors();
350                                     break;
351                                 }
352                             }
353                         } else {
354                             // VM doesn't support monitor usage monitoring
355                             ti = threadMBean.getThreadInfo(threadID, Integer.MAX_VALUE);
356                         }
357                         if (ti != null) {
358                             if (ti.getLockName() == null) {
359                                 sb.append(Resources.getText("Name State",
360                                               ti.getThreadName(),
361                                               ti.getThreadState().toString()));
362                             } else if (ti.getLockOwnerName() == null) {
363                                 sb.append(Resources.getText("Name State LockName",
364                                               ti.getThreadName(),
365                                               ti.getThreadState().toString(),
366                                               ti.getLockName()));
367                             } else {
368                                 sb.append(Resources.getText("Name State LockName LockOwner",
369                                               ti.getThreadName(),
370                                               ti.getThreadState().toString(),
371                                               ti.getLockName(),
372                                               ti.getLockOwnerName()));
373                             }
374                             sb.append(Resources.getText("BlockedCount WaitedCount",
375                                               ti.getBlockedCount(),
376                                               ti.getWaitedCount()));
377                             sb.append(Resources.getText("Stack trace"));
378                             int index = 0;
379                             for (StackTraceElement e : ti.getStackTrace()) {
380                                 sb.append(e.toString()+"\n");
381                                 if (monitors != null) {
382                                     for (MonitorInfo mi : monitors) {
383                                         if (mi.getLockedStackDepth() == index) {
384                                             sb.append(Resources.getText("Monitor locked", mi.toString()));
385                                         }
386                                     }
387                                 }
388                                 index++;
389                             }
390                         }
391                     } catch (IOException ex) {
392                         // Ignore
393                     } catch (UndeclaredThrowableException e) {
394                         proxyClient.markAsDead();
395                     }
396                     final String text = sb.toString();
397                     SwingUtilities.invokeLater(new Runnable() {
398                         public void run() {
399                             textArea.setText(text);
400                             textArea.setCaretPosition(0);
401                         }
402                     });
403                 }
404             });
405         }
406     }
407 
408     private void doUpdate() {
409         workerAdd(new Runnable() {
410             public void run() {
411                 update();
412             }
413         });
414     }
415 
416 
417     private void detectDeadlock() {
418         workerAdd(new Runnable() {
419             public void run() {
420                 try {
421                     final Long[][] deadlockedThreads = getDeadlockedThreadIds();
422 
423                     if (deadlockedThreads == null || deadlockedThreads.length == 0) {
424                         // Display message for 30 seconds. Do it on a separate thread so
425                         // the sleep won't hold up the worker queue.
426                         // This will be replaced later by separate statusbar logic.
427                         new Thread() {
428                             public void run() {
429                                 try {
430                                     SwingUtilities.invokeAndWait(new Runnable() {
431                                         public void run() {
432                                             String msg = Resources.getText("No deadlock detected");
433                                             messageLabel.setText(msg);
434                                             threadListTabbedPane.revalidate();
435                                         }
436                                     });
437                                     sleep(30 * 1000);
438                                 } catch (InterruptedException ex) {
439                                     // Ignore
440                                 } catch (InvocationTargetException ex) {
441                                     // Ignore
442                                 }
443                                 SwingUtilities.invokeLater(new Runnable() {
444                                     public void run() {
445                                         messageLabel.setText("");
446                                     }
447                                 });
448                             }
449                         }.start();
450                         return;
451                     }
452 
453                     SwingUtilities.invokeLater(new Runnable() {
454                         public void run() {
455                             // Remove old deadlock tabs
456                             while (threadListTabbedPane.getTabCount() > 1) {
457                                 threadListTabbedPane.removeTabAt(1);
458                             }
459 
460                             if (deadlockedThreads != null) {
461                                 for (int i = 0; i < deadlockedThreads.length; i++) {
462                                     DefaultListModel listModel = new DefaultListModel();
463                                     JTextArea textArea = new JTextArea();
464                                     textArea.setBorder(thinEmptyBorder);
465                                     textArea.setEditable(false);
466                                     setAccessibleName(textArea,
467                                         getText("ThreadTab.threadInfo.accessibleName"));
468                                     JList list = new ThreadJList(listModel, textArea);
469                                     JScrollPane threadlistSP = new JScrollPane(list);
470                                     JScrollPane textAreaSP = new JScrollPane(textArea);
471                                     threadlistSP.setBorder(null);
472                                     textAreaSP.setBorder(null);
473                                     JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
474                                                                                  threadlistSP, textAreaSP);
475                                     splitPane.setOneTouchExpandable(true);
476                                     splitPane.setBorder(null);
477                                     splitPane.setDividerLocation(threadsSplitPane.getDividerLocation());
478                                     String tabName;
479                                     if (deadlockedThreads.length > 1) {
480                                         tabName = Resources.getText("deadlockTabN", i+1);
481                                     } else {
482                                         tabName = Resources.getText("deadlockTab");
483                                     }
484                                     threadListTabbedPane.addTab(tabName, splitPane);
485 
486                                     for (long t : deadlockedThreads[i]) {
487                                         listModel.addElement(t);
488                                     }
489                                 }
490                                 threadListTabbedPane.setSelectedIndex(1);
491                             }
492                         }
493                     });
494                 } catch (IOException e) {
495                     // Ignore
496                 } catch (UndeclaredThrowableException e) {
497                     vmPanel.getProxyClient().markAsDead();
498                 }
499             }
500         });
501     }
502 
503 
504     // Return deadlocked threads or null
505     public Long[][] getDeadlockedThreadIds() throws IOException {
506         ProxyClient proxyClient = vmPanel.getProxyClient();
507         ThreadMXBean threadMBean = proxyClient.getThreadMXBean();
508 
509         long[] ids = proxyClient.findDeadlockedThreads();
510         if (ids == null) {
511             return null;
512         }
513         ThreadInfo[] infos = threadMBean.getThreadInfo(ids, Integer.MAX_VALUE);
514 
515         List<Long[]> dcycles = new ArrayList<Long[]>();
516         List<Long> cycle = new ArrayList<Long>();
517 
518         // keep track of which thread is visited
519         // one thread can only be in one cycle
520         boolean[] visited = new boolean[ids.length];
521 
522         int deadlockedThread = -1; // Index into arrays
523         while (true) {
524             if (deadlockedThread < 0) {
525                 if (cycle.size() > 0) {
526                     // a cycle found
527                     dcycles.add(cycle.toArray(new Long[0]));
528                     cycle = new ArrayList<Long>();
529                 }
530                 // start a new cycle from a non-visited thread
531                 for (int j = 0; j < ids.length; j++) {
532                     if (!visited[j]) {
533                         deadlockedThread = j;
534                         visited[j] = true;
535                         break;
536                     }
537                 }
538                 if (deadlockedThread < 0) {
539                     // done
540                     break;
541                 }
542             }
543 
544             cycle.add(ids[deadlockedThread]);
545             long nextThreadId = infos[deadlockedThread].getLockOwnerId();
546             for (int j = 0; j < ids.length; j++) {
547                 ThreadInfo ti = infos[j];
548                 if (ti.getThreadId() == nextThreadId) {
549                      if (visited[j]) {
550                          deadlockedThread = -1;
551                      } else {
552                          deadlockedThread = j;
553                          visited[j] = true;
554                      }
555                      break;
556                 }
557             }
558         }
559         return dcycles.toArray(new Long[0][0]);
560     }
561 
562 
563 
564 
565 
566     // ActionListener interface
567     public void actionPerformed(ActionEvent evt) {
568         String cmd = ((AbstractButton)evt.getSource()).getActionCommand();
569 
570         if (cmd == "detectDeadlock") {
571             messageLabel.setText("");
572             detectDeadlock();
573         }
574     }
575 
576 
577 
578     // DocumentListener interface
579 
580     public void insertUpdate(DocumentEvent e) {
581         doUpdate();
582     }
583 
584     public void removeUpdate(DocumentEvent e) {
585         doUpdate();
586     }
587 
588     public void changedUpdate(DocumentEvent e) {
589         doUpdate();
590     }
591 
592 
593 
594     private class ThreadJList extends JList {
595         private JTextArea textArea;
596 
597         ThreadJList(DefaultListModel listModel, JTextArea textArea) {
598             super(listModel);
599 
600             this.textArea = textArea;
601 
602             setBorder(thinEmptyBorder);
603 
604             addListSelectionListener(ThreadTab.this);
605             setCellRenderer(new DefaultListCellRenderer() {
606                 public Component getListCellRendererComponent(JList list, Object value, int index,
607                                                               boolean isSelected, boolean cellHasFocus) {
608                     super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
609 
610                     if (value != null) {
611                         String name = nameCache.get(value);
612                         if (name == null) {
613                             name = value.toString();
614                         }
615                         setText(name);
616                     }
617                     return this;
618                 }
619             });
620         }
621 
622         public Dimension getPreferredSize() {
623             Dimension d = super.getPreferredSize();
624             d.width = Math.max(d.width, 100);
625             return d;
626         }
627     }
628 
629     private class PromptingTextField extends JTextField implements FocusListener {
630         private String prompt;
631         boolean promptRemoved = false;
632         Color fg;
633 
634         public PromptingTextField(String prompt, int columns) {
635             super(prompt, columns);
636 
637             this.prompt = prompt;
638             updateForeground();
639             addFocusListener(this);
640             setAccessibleName(this, prompt);
641         }
642 
643         @Override
644         public void revalidate() {
645             super.revalidate();
646             updateForeground();
647         }
648 
649         private void updateForeground() {
650             this.fg = UIManager.getColor("TextField.foreground");
651             if (promptRemoved) {
652                 setForeground(fg);
653             } else {
654                 setForeground(Color.gray);
655             }
656         }
657 
658         public String getText() {
659             if (!promptRemoved) {
660                 return "";
661             } else {
662                 return super.getText();
663             }
664         }
665 
666         public void focusGained(FocusEvent e) {
667             if (!promptRemoved) {
668                 setText("");
669                 setForeground(fg);
670                 promptRemoved = true;
671             }
672         }
673 
674         public void focusLost(FocusEvent e) {
675             if (promptRemoved && getText().equals("")) {
676                 setText(prompt);
677                 setForeground(Color.gray);
678                 promptRemoved = false;
679             }
680         }
681 
682     }
683 
684     OverviewPanel[] getOverviewPanels() {
685         if (overviewPanel == null) {
686             overviewPanel = new ThreadOverviewPanel();
687         }
688         return new OverviewPanel[] { overviewPanel };
689     }
690 
691 
692     private static class ThreadOverviewPanel extends OverviewPanel {
693         ThreadOverviewPanel() {
694             super(getText("Threads"), threadCountKey, threadCountName, null);
695         }
696 
697         private void updateThreadsInfo(long tlCount, long tpCount, long ttCount, long timeStamp) {
698             getPlotter().addValues(timeStamp, tlCount);
699             getInfoLabel().setText(getText(infoLabelFormat, tlCount, tpCount, ttCount));
700         }
701     }
702 }